/*
 * Galaxium Messenger
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;

using Anculus.Core;

using Galaxium.Core;
using Galaxium.Gui;
using Galaxium.Protocol;

namespace Galaxium.AdiumThemes
{
	public delegate string SubstitutionDelegate (string param, object val);
	
	public class AdiumSubstitutionCollection : Dictionary<string, SubstitutionDelegate>
	{
	}
	
	public class AdiumMessageStyle
	{
		static Regex _regexSubstitutions = new Regex (@"%([A-Za-z0-9]*)(\{(.*)\})?%", RegexOptions.Compiled);
		
		static string _defaultTemplateFilename;
		
		string _archiveFilename;
		string _cacheDir;
		string _contentsDir;
		string _resourcesDir;
		string _variantsDir;
		string _incomingDir;
		string _outgoingDir;
		string _templatesDir;
		string _plistFilename;
		string _mainCssFilename;
		string _headerFilename;
		string _footerFilename;
		string _statusFilename;
		string _defaultIncomingDisplayIconFilename;
		string _defaultOutgoingDisplayIconFilename;
		
		PListDict _plistRoot;
		
		string _name;
		string _infoString;
		string _defaultVariant;
		string _displayNameForNoVariant;
		
		List<string> _variants = new List<string> ();
		
		public string ArchiveFilename
		{
			get { return _archiveFilename; }
		}
		
		public string Name
		{
			get { return _name; }
		}
		
		public string InfoString
		{
			get { return _infoString; }
		}
		
		public string DisplayNameForNoVariant
		{
			get { return _displayNameForNoVariant; }
		}
		
		public string DefaultIncomingDisplayIconFilename
		{
			get { return _defaultIncomingDisplayIconFilename; }
		}
		
		public string DefaultOutgoingDisplayIconFilename
		{
			get { return _defaultOutgoingDisplayIconFilename; }
		}
		
		public List<string> Variants
		{
			get { return _variants; }
		}
		
		public string DefaultVariant
		{
			get
			{
				if ((!string.IsNullOrEmpty (_defaultVariant)) && _variants.Contains (_defaultVariant))
					return _defaultVariant;
				
				if (!string.IsNullOrEmpty (_displayNameForNoVariant))
					return _displayNameForNoVariant;
				
				if (_variants.Count > 0)
					return _variants[0];
				
				return string.Empty;
			}
		}
		
		static AdiumMessageStyle ()
		{
			// Write the default template to disk
			
			_defaultTemplateFilename = Path.Combine (AdiumMessageStyleFactory._cacheStylesPath, "Template.html");
			
			Stream templateStream = Assembly.GetCallingAssembly ().GetManifestResourceStream ("Template.html");
			byte[] data = new byte[templateStream.Length];
			templateStream.Read (data, 0, data.Length);
			
			File.WriteAllBytes (_defaultTemplateFilename, data);
		}
		
		public AdiumMessageStyle (string archiveFilename)
		{
			_archiveFilename = archiveFilename;
			
			Extract ();
			FindDirectories ();
			LoadPList ();
			FindVariants ();
		}
		
		void Extract ()
		{
			ThrowUtility.ThrowIfFalse ("Unsupported archive " + _archiveFilename, ArchiveUtility.IsFileSupported (_archiveFilename));
			
			_cacheDir = Path.Combine (AdiumMessageStyleFactory._cacheStylesPath, ArchiveUtility.GetArchiveNameWithoutExtension (_archiveFilename));
			ThrowUtility.ThrowIfFalse ("Unable to extract " + _archiveFilename, ArchiveUtility.Extract (_archiveFilename, _cacheDir));
		}
		
		void DeleteCache ()
		{
			if (Directory.Exists (_cacheDir))
				Directory.Delete (_cacheDir, true);
		}
		
		void FindDirectories ()
		{
			_contentsDir = ArchiveUtility.FindContentsDir (_cacheDir, ".adiummessagestyle");
			ThrowUtility.ThrowIfEmpty ("Unable to find contents directory of " + _archiveFilename, _contentsDir);
			
			_plistFilename = FileNoCase (Path.Combine (_contentsDir, "Info.plist"));
			ThrowUtility.ThrowIfFalse ("Unable to find Info.plist of " + _archiveFilename, File.Exists (_plistFilename));
			
			_resourcesDir = DirNoCase (Path.Combine (_contentsDir, "Resources"));
			ThrowUtility.ThrowIfFalse ("Unable to find resources directory of " + _archiveFilename, Directory.Exists (_resourcesDir));
			
			_statusFilename = FileNoCase (Path.Combine (_resourcesDir, "Status.html"));
			ThrowUtility.ThrowIfFalse ("Unable to find Status.html of " + _archiveFilename, File.Exists (_statusFilename));
			
			_mainCssFilename = FileNoCase (Path.Combine (_resourcesDir, "main.css"));
			_headerFilename = FileNoCase (Path.Combine (_resourcesDir, "Header.html"));
			_footerFilename = FileNoCase (Path.Combine (_resourcesDir, "Footer.html"));
						
			_variantsDir = DirNoCase (Path.Combine (_resourcesDir, "Variants"));
			
			_incomingDir = DirNoCase (Path.Combine (_resourcesDir, "Incoming"));
			ThrowUtility.ThrowIfFalse ("Unable to find incoming directory of " + _archiveFilename, Directory.Exists (_incomingDir));
			
			_outgoingDir = DirNoCase (Path.Combine (_resourcesDir, "Outgoing"));
			
			_defaultIncomingDisplayIconFilename = FileNoCase (Path.Combine (_incomingDir, "buddy_icon.png"));
			if (!File.Exists (_defaultIncomingDisplayIconFilename))
				_defaultIncomingDisplayIconFilename = FileNoCase (Path.Combine (_resourcesDir, "incoming_icon.png"));
			
			if (!string.IsNullOrEmpty (_outgoingDir))
				_defaultOutgoingDisplayIconFilename = FileNoCase (Path.Combine (_outgoingDir, "buddy_icon.png"));
			if (string.IsNullOrEmpty (_defaultOutgoingDisplayIconFilename) || (!File.Exists (_defaultOutgoingDisplayIconFilename)))
			{
				_defaultOutgoingDisplayIconFilename = FileNoCase (Path.Combine (_resourcesDir, "outgoing_icon.png"));
				if (!File.Exists (_defaultOutgoingDisplayIconFilename))
					_defaultOutgoingDisplayIconFilename = _defaultIncomingDisplayIconFilename;
			}
			
			_templatesDir = Path.Combine (Path.GetDirectoryName (_contentsDir), "Templates");
			BaseUtility.CreateDirectoryIfNeeded (_templatesDir);
		}
		
		void LoadPList ()
		{
			PList plist = new PList (_plistFilename);
			_plistRoot = plist[0] as PListDict;
			
			_name = _plistRoot.Value<string> ("CFBundleName", Path.GetFileNameWithoutExtension (_archiveFilename));
			_infoString = _plistRoot.Value<string> ("CFBundleGetInfoString", string.Empty);
			_defaultVariant = _plistRoot.Value<string> ("DefaultVariant", string.Empty);
			_displayNameForNoVariant = _plistRoot.Value<string> ("DisplayNameForNoVariant", string.Empty);
		}
		
		T PListValue<T> (string keyName, string variant, T def)
		{
			if (_plistRoot.ContainsKey (keyName + ":" + variant))
				return _plistRoot.Value<T> (keyName + ":" + variant, def);
			
			return _plistRoot.Value<T> (keyName, def);
		}
		
#region PList Value Methods
		public bool GetAllowTextColors (string variant)
		{
			return PListValue<bool> ("AllowTextColors", variant, true);
		}
		
		public string GetDefaultBackgroundColor (string variant)
		{
			return PListValue<string> ("DefaultBackgroundColor", variant,  "FFFFFF");
		}
		
		public string GetDefaultFontFamily (string variant)
		{
			return PListValue<string> ("DefaultFontFamily", variant, string.Empty);
		}
		
		public int GetDefaultFontSize (string variant)
		{
			return PListValue<int> ("DefaultFontSize", variant, 0);
		}
		
		public bool GetDisableCustomBackground (string variant)
		{
			return PListValue<bool> ("DisableCustomBackground", variant, false);
		}
		
		public string GetImageMask (string variant)
		{
			return PListValue<string> ("ImageMask", variant, string.Empty);
		}
		
		public bool ShowsUserIcons (string variant)
		{
			return PListValue<bool> ("ShowsUserIcons", variant, true);
		}
#endregion
		
		void FindVariants ()
		{
			_variants.Clear ();
			
			if (!string.IsNullOrEmpty (_displayNameForNoVariant))
				_variants.Add (_displayNameForNoVariant);
			
			if (string.IsNullOrEmpty (_variantsDir))
				return;
			
			foreach (string filename in Directory.GetFiles (_variantsDir, "*.css"))
			{
				string name = Path.GetFileNameWithoutExtension (filename);
				
				if (name != _displayNameForNoVariant)
					_variants.Add (name);
			}
		}
		
		string FindTemplate ()
		{
			string file = Path.Combine (_resourcesDir, "template.html");
			
			if (File.Exists (file))
				return file;
			
			return _defaultTemplateFilename;
		}
		
		string GenerateBodyBackground ()
		{
			string css = string.Empty;
			
			//TODO: set background images here
			
			// This is a hack to make styles look reasonable in gecko
			if (HTMLUtility.WidgetType.Name.Contains ("Gecko"))
				css += "font-size: 11px; overflow: auto; ";
			
			return css;
		}
		
		public string GenerateBaseHtmlFile (string variant, AdiumSubstitutionCollection headerFooterSubstitutions)
		{
			string destFilename = Path.Combine (_templatesDir, variant + ".html");
			string html = File.ReadAllText (FindTemplate ());
			
			html = BaseUtility.ReplaceString (html, "%@", "file://" + _resourcesDir + "/");
			
			if (CountOccurances (html, "%@") > 3)
			{
				html = BaseUtility.ReplaceString (html, "%@", string.IsNullOrEmpty (_mainCssFilename) ? string.Empty : string.Format ("@import url(\"file://{0}\");", _mainCssFilename));
				html = BaseUtility.ReplaceString (html, "%@", FindVariantCssFile (variant));
			}
			else
				html = BaseUtility.ReplaceString (html, "%@", string.IsNullOrEmpty (_mainCssFilename) ? string.Empty : string.Format ("@import url(\"file://{0}\");", _mainCssFilename));
			
			html = BaseUtility.ReplaceString (html, "%@", GenerateHeader (headerFooterSubstitutions));
			html = BaseUtility.ReplaceString (html, "%@", GenerateFooter (headerFooterSubstitutions));
			
			html = BaseUtility.ReplaceString (html, "==bodyBackground==", GenerateBodyBackground ());
			
			File.WriteAllText (destFilename, html);
			
			//Log.Debug ("Saved to {0}", destFilename);
			
			return destFilename;
		}
		
		string GenerateHeader (AdiumSubstitutionCollection substitutions)
		{
			if (string.IsNullOrEmpty (_headerFilename))
				return string.Empty;
			
			return Substitute (substitutions, File.ReadAllText (_headerFilename), null);
		}
		
		string GenerateFooter (AdiumSubstitutionCollection substitutions)
		{
			if (string.IsNullOrEmpty (_footerFilename))
				return string.Empty;
			
			return Substitute (substitutions, File.ReadAllText (_footerFilename), null);
		}
		
		public string FindVariantCssFile (string variant)
		{
			if (string.IsNullOrEmpty (_variantsDir))
				return string.Empty;
			
			if (string.IsNullOrEmpty (variant))
				return string.Empty;
			
			return Path.Combine (_variantsDir, variant + ".css");
		}
		
		public string GenerateMessageHtml (ITextMessage msg, bool newContact, bool context, AdiumSubstitutionCollection substitutions)
		{
			string file = FindMessageFile (!msg.Source.Local, newContact, context);
			
			if (!File.Exists (file))
				return string.Empty;
			
			return Substitute (substitutions, File.ReadAllText (file), msg).Replace ("\r\n", "\\r\\n").Replace ("\n", "\\n");
		}
		
		public string GenerateStatusHtml (string msg, AdiumSubstitutionCollection substitutions)
		{
			return Substitute (substitutions, File.ReadAllText (_statusFilename), msg).Replace ("\r\n", "\\r\\n").Replace ("\n", "\\n");
		}
		
		public string FindMessageFile (bool incoming, bool newContact, bool context)
		{
			string dir;
			string filename = newContact ? string.Empty : "Next";
			
			if (incoming)
				dir = _incomingDir;
			else
				dir = string.IsNullOrEmpty (_outgoingDir) ? _incomingDir : _outgoingDir;

			filename += context ? "Context" : "Content";
			filename += ".html";
			
			filename = Path.Combine (dir, filename);
			
			if (File.Exists (filename))
				return filename;
			
			if (context)
				return FindMessageFile (incoming, newContact, false);
			if (!newContact)
				return FindMessageFile (incoming, true, context);
			if (!incoming)
				return FindMessageFile (true, newContact, context);
			
			return null;
		}
		
		int CountOccurances (string str, string sub)
		{
			ThrowUtility.ThrowIfNull ("str", str);
			ThrowUtility.ThrowIfEmpty ("sub", sub);
			
			int count = 0;
			int pos = str.IndexOf (sub);
			
			while (pos >= 0)
			{
				count++;
				pos = str.IndexOf (sub, pos + sub.Length);
			}
			
			return count;
		}
		
		string Substitute (AdiumSubstitutionCollection substitutions, string str, object val)
		{
			MatchCollection matches = _regexSubstitutions.Matches (str);
			
			string ret = string.Empty;
			int pos = 0;
			
			foreach (Match match in matches)
			{
				string name = match.Groups[1].Value;
				string param = match.Groups[3].Value;
				
				ret += str.Substring (pos, match.Index - pos);
				
				if (substitutions.ContainsKey (name))
					ret += substitutions[name] (param, val);
				else
					Log.Warn ("Unknown keyword '{0}', param '{1}'", name, param);
				
				pos = match.Index + match.Length;
			}
			
			ret += str.Substring (pos);
			
			return ret;
		}
		
		string DirNoCase (string dir)
		{
			if (Directory.Exists (dir))
				return dir;
			
			foreach (string subDir in Directory.GetDirectories (Path.GetDirectoryName (dir)))
				if (subDir.Equals (dir, StringComparison.InvariantCultureIgnoreCase))
					return subDir;
			
			return string.Empty;
		}
		
		string FileNoCase (string filename)
		{
			if (File.Exists (filename))
				return filename;
			
			foreach (string dirFile in Directory.GetFiles (Path.GetDirectoryName (filename)))
				if (dirFile.Equals (filename, StringComparison.InvariantCultureIgnoreCase))
					return dirFile;
			
			return string.Empty;
		}
	}
}
